#include "eeprom.h"
#include "board_config.h"

struct eeprom {
	uint32_t CurPageBase;
	uint32_t CurSwapPageBase;
	uint16_t Status;
} eeprom_info = {
	.CurPageBase = 0,
	.CurSwapPageBase = 0,
	.Status = EEPROM_NOT_INIT
};

static uint32_t EE_FindPageByStatus(uint32_t sector, uint16_t status);
static uint16_t EE_EraseSector(uint32_t pageBase);
static uint16_t EE_GetVariablesCount(uint16_t skipAddress);
static uint16_t EE_PageTransfer(uint32_t newPage, uint32_t oldPage, uint16_t skipAddress);
static uint16_t EE_VerifyPageFullWriteVariable(uint16_t address, uint16_t data);
static uint16_t EE_Format(void);

/**
 * @brief initialze flash emulated eeprom
 * @param  none
 * @retval Success or error status:
 *           - EEPROM_OK: if succeed
 *           - error code define by HAL_StatusTypeDef
 */
uint16_t eeprom_init(void)
{
	uint16_t FlashStatus;
	HAL_FLASH_Unlock();

	eeprom_info.CurPageBase = EE_FindPageByStatus(EEPROM_SECTOR0_BASE, EEPROM_VALID_PAGE);
	if (eeprom_info.CurPageBase == 0) { // need format
		return EE_Format();
	}

	if(eeprom_info.CurPageBase == EEPROM_SECTOR1_BASE + EEPROM_SECTOR_SIZE - EEPROM_PAGE_SIZE) { //the last page
		eeprom_info.CurSwapPageBase = EEPROM_SECTOR0_BASE;
	} else {
		eeprom_info.CurSwapPageBase = eeprom_info.CurPageBase + EEPROM_PAGE_SIZE;
	}

	switch (*(__IO uint16_t*)eeprom_info.CurSwapPageBase) {
		case EEPROM_ERASED:
			eeprom_info.Status = EEPROM_OK;
			return EEPROM_OK;
		/* must be a bug, recover */
		case EEPROM_RECEIVE_DATA:
			FlashStatus = EE_PageTransfer(eeprom_info.CurSwapPageBase, eeprom_info.CurPageBase, 0xffff);
			if (FlashStatus == EEPROM_OK) {
				eeprom_info.Status = EEPROM_OK;
				return EEPROM_OK;
			} /* no break here because we should do more work */
		case EEPROM_VALID_PAGE:
		case EEPROM_IGNORED_PAGE:
		default:
			if (eeprom_info.CurPageBase >= EEPROM_SECTOR0_BASE && eeprom_info.CurPageBase < EEPROM_SECTOR1_BASE) {
				FlashStatus = EE_EraseSector(EEPROM_SECTOR1_BASE);
				if (FlashStatus != EEPROM_OK)
					return FlashStatus;

				FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, EEPROM_SECTOR1_BASE, EEPROM_RECEIVE_DATA);
				if (FlashStatus != HAL_OK)
					return FlashStatus;

				FlashStatus = EE_PageTransfer(EEPROM_SECTOR1_BASE, eeprom_info.CurPageBase, 0xffff);
				if (FlashStatus != EEPROM_OK)
					return FlashStatus;

				FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, EEPROM_SECTOR1_BASE, EEPROM_VALID_PAGE);
				if (FlashStatus != HAL_OK)
					return FlashStatus;

				FlashStatus = EE_EraseSector(EEPROM_SECTOR0_BASE);
				if (FlashStatus != EEPROM_OK)
					return FlashStatus;

				eeprom_info.CurPageBase = EEPROM_SECTOR1_BASE;
				eeprom_info.CurSwapPageBase = EEPROM_SECTOR1_BASE + EEPROM_PAGE_SIZE;
				eeprom_info.Status = EEPROM_OK;
				return EEPROM_OK;
			}

			FlashStatus = EE_EraseSector(EEPROM_SECTOR0_BASE);
			if (FlashStatus != EEPROM_OK)
				return FlashStatus;

			FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, EEPROM_SECTOR0_BASE, EEPROM_RECEIVE_DATA);
			if (FlashStatus != HAL_OK)
				return FlashStatus;

			FlashStatus = EE_PageTransfer(EEPROM_SECTOR0_BASE, eeprom_info.CurPageBase, 0xffff);
			if (FlashStatus != EEPROM_OK)
				return FlashStatus;

			FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, EEPROM_SECTOR0_BASE, EEPROM_VALID_PAGE);
			if (FlashStatus != HAL_OK)
				return FlashStatus;

			FlashStatus = EE_EraseSector(EEPROM_SECTOR1_BASE);
			if (FlashStatus != EEPROM_OK)
				return FlashStatus;

			eeprom_info.CurPageBase = EEPROM_SECTOR0_BASE;
			eeprom_info.CurSwapPageBase = EEPROM_SECTOR0_BASE + EEPROM_PAGE_SIZE;
			eeprom_info.Status = EEPROM_OK;
			return EEPROM_OK;
	}
	return EEPROM_OK;
}

/**
 * @brief	Returns the last stored variable data, if found,
 *		which correspond to the passed virtual address
 * @param  address: Variable virtual address
 * @param  data: Pointer to data variable
 * @retval Success or error status:
 *           - EEPROM_OK: if variable was found
 *           - EEPROM_BAD_ADDRESS: if the variable was not found
 *           - EEPROM_NOT_INIT: if eeprom is not initialized
 */
uint16_t eeprom_read(uint16_t address, uint16_t *data)
{
	uint32_t pageBase, pageEnd;

	if (eeprom_info.Status != EEPROM_OK)
		return eeprom_info.Status;

	// Set default data (empty EEPROM)
	*data = EEPROM_DEFAULT_DATA;

	pageBase = eeprom_info.CurPageBase;
	// Get the valid Page end Address
	pageEnd = pageBase + ((uint32_t)(EEPROM_PAGE_SIZE - 2));

	// Check each active page address starting from end
	for (pageBase += 6; pageEnd >= pageBase; pageEnd -= 4) {
		if ((*(__IO uint16_t*)pageEnd) == address)		// Compare the read address with the virtual address
		{
			*data = (*(__IO uint16_t*)(pageEnd - 2));		// Get content of Address-2 which is variable value
			return EEPROM_OK;
		}
	}

	return EEPROM_BAD_ADDRESS;
}

/**
 * @brief  Writes/upadtes variable data in EEPROM.
 * @param  address: Variable virtual address
 * @param  data: 16 bit data to be written
 * @retval Success or error status:
 *			- EEPROM_OK: on success
 *			- EEPROM_BAD_ADDRESS: if address = 0xFFFF
 *			- EEPROM_NOT_INIT: if eeprom is not initialized
 *			- EEPROM_OUT_SIZE: if no empty EEPROM variables
 *			- Flash error code define by HAL_StatusTypeDef 
 */
uint16_t eeprom_write(uint16_t address, uint16_t data)
{
	if (eeprom_info.Status != EEPROM_OK)
		return eeprom_info.Status;

	if (address == 0xFFFF)
		return EEPROM_BAD_ADDRESS;

	// Write the variable virtual address and value in the EEPROM
	return EE_VerifyPageFullWriteVariable(address, data);
}

/**
 * @brief  Erase page with increment erase counter (page + 2)
 * @param  page base address
 * @retval Success or error
 *	-EEPROM_OK: success erase
 *	-Flash error code defined by HAL_StatusTypeDef
 */
uint16_t EE_EraseSector(uint32_t pageBase)
{
	uint16_t FlashStatus;
	FLASH_EraseInitTypeDef erase_init_struct;
	uint32_t sector_error = 0;
	uint32_t sector_erase;
	uint16_t data; 

	if ((pageBase < EEPROM_SECTOR1_BASE) && (pageBase >= EEPROM_SECTOR0_BASE))
		sector_erase = EEPROM_SECTOR0;
	else if ((pageBase >= EEPROM_SECTOR1_BASE) && (pageBase < EEPROM_SECTOR1_BASE + EEPROM_SECTOR_SIZE))
		sector_erase = EEPROM_SECTOR1;
	else 
		return EEPROM_BAD_ADDRESS;

	/* get page status */
	data = *(__IO uint16_t*)(pageBase);

	if ((data == EEPROM_ERASED) || (data == EEPROM_VALID_PAGE) 
			|| (data == EEPROM_RECEIVE_DATA) 
			|| (data == EEPROM_IGNORED_PAGE)) {
		/* increase the erase counter */
		data = (*(__IO uint16_t*)(pageBase + 2)) + 1;
	} else {
		data = 0;
	}

	erase_init_struct.TypeErase = FLASH_TYPEERASE_SECTORS;
	erase_init_struct.VoltageRange = EEPROM_VOLTAGE_RANGE;
	erase_init_struct.Sector = sector_erase;
	erase_init_struct.NbSectors = 1;

	FlashStatus = HAL_FLASHEx_Erase(&erase_init_struct, &sector_error);
	if (FlashStatus != HAL_OK)
		return FlashStatus;

	FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, pageBase + 2, data);
	if (FlashStatus != HAL_OK)
		return FlashStatus;

	return EEPROM_OK;
}

/**
 * @brief  Find valid Page for write or read operation
 * @param  	start_addr: page base to start with, TODO must be pageBase
 * 		status:	the status of the page to find
 * 			-EEPROM_ERASED
 * 			-EEPROM_RECEIVE_DATA
 * 			-EEPROM_VALID_PAGE
 * 			-EEPROM_IGNORED_PAGE
 * @retval page address  or 0 in case of no target page was found
 */
uint32_t EE_FindPageByStatus(uint32_t start_addr, uint16_t status)
{
	uint32_t pageBase;
	uint32_t end_addr = EEPROM_SECTOR1_BASE + EEPROM_SECTOR_SIZE; 

	if (status != EEPROM_VALID_PAGE
			&& status != EEPROM_ERASED
			&& status != EEPROM_RECEIVE_DATA
			&& status != EEPROM_IGNORED_PAGE)
		return 0;

	for (pageBase = start_addr; pageBase < end_addr; pageBase += EEPROM_PAGE_SIZE)
	{
		if (status == *(__IO uint16_t *)pageBase)
			return pageBase;
	}
	return 0;
}

/**
 * @brief  Calculate unique variables in EEPROM
 * @param  start: address of first slot to check (page + 4)
 * @param	end: page end address
 * @param	address: 16 bit virtual address of the variable to excluse (or 0XFFFF)
 * @retval count of variables
 */
uint16_t EE_GetVariablesCount(uint16_t skipAddress)
{
	uint16_t varAddress, nextAddress;
	uint32_t idx;
	uint32_t pageBase = eeprom_info.CurPageBase;
	uint32_t pageEnd = eeprom_info.CurPageBase + EEPROM_PAGE_SIZE;
	uint16_t mycount = 0;

	for (pageBase += 6; pageBase < pageEnd; pageBase += 4) {
		varAddress = (*(__IO uint16_t*)pageBase);
		if (varAddress == 0xFFFF || varAddress == skipAddress)
			continue;

		mycount++;
		for(idx = pageBase + 4; idx < pageEnd; idx += 4) {
			nextAddress = (*(__IO uint16_t*)idx);
			if (nextAddress == varAddress) {
				mycount--;
				break;
			}
		}
	}
	return mycount;
}

/**
 * @brief  here we assume that newPage is valid, this function doesn't manage page status
 * @param  newPage: new page base address
 * @param	oldPage: old page base address
 *	@param	SkipAddress: 16 bit virtual address of the variable (or 0xFFFF)
 * @retval Success or error status:
 *           - FLASH_COMPLETE: on success
 *           - EEPROM_OUT_SIZE: if valid new page is full
 *           - Flash error code: on write Flash error
 */
uint16_t EE_PageTransfer(uint32_t newPage, uint32_t oldPage, uint16_t skipAddress)
{
	uint32_t oldEnd, newEnd;
	uint32_t oldIdx, newIdx, idx;
	uint16_t address, data, found;
	HAL_StatusTypeDef FlashStatus;

	// Transfer process: transfer variables from old to the new active page
	newEnd = newPage + ((uint32_t)EEPROM_PAGE_SIZE);
	// Find first free element in new page
	for (newIdx = newPage + 4; newIdx < newEnd; newIdx += 4)
		if ((*(__IO uint32_t*)newIdx) == 0xFFFFFFFF)	
			break;									
	if (newIdx >= newEnd)
		return EEPROM_OUT_SIZE;

	oldEnd = oldPage + 4;
	oldIdx = oldPage + (uint32_t)(EEPROM_PAGE_SIZE - 2);

	for (; oldIdx > oldEnd; oldIdx -= 4)
	{
		address = *(__IO uint16_t*)oldIdx;
		if (address == 0xFFFF || address == skipAddress)
			continue;						// it's means that power off after write data

		found = 0;
		//check exiting element, this must be newer than the one in the old page
		for (idx = newPage + 6; idx < newIdx; idx += 4) {
			if ((*(__IO uint16_t*)(idx)) == address) {
				found = 1;
				break;
			}
		}

		if (found == 1) {
			continue;
		}

		if (newIdx < newEnd) {
			data = (*(__IO uint16_t*)(oldIdx - 2));

			FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, newIdx, data);
			if (FlashStatus != HAL_OK)
				return FlashStatus;

			FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, newIdx + 2, address);
			if (FlashStatus != HAL_OK)
				return FlashStatus;

			newIdx += 4;
		} else {
			return EEPROM_OUT_SIZE;
		}
	}
	return EEPROM_OK;
}

/**
 * @brief  Verify if active page is full and Writes variable in EEPROM.
 * @param  Address: 16 bit virtual address of the variable
 * @param  Data: 16 bit data to be written as variable value
 * @retval Success or error status:
 *           - FLASH_COMPLETE: on success
 *           - EEPROM_PAGE_FULL: if valid page is full (need page transfer)
 *           - EEPROM_NO_VALID_PAGE: if no valid page was found
 *           - EEPROM_OUT_SIZE: if EEPROM size exceeded
 *           - Flash error code: on write Flash error
 */
uint16_t EE_VerifyPageFullWriteVariable(uint16_t address, uint16_t data)
{
	uint16_t FlashStatus;
	uint32_t idx, curPageEnd;
	uint16_t old_data;
	uint16_t var_num;

	// Get the valid Page end Address
	curPageEnd = eeprom_info.CurPageBase + EEPROM_PAGE_SIZE;	

	/* has an old value */
	for (idx = curPageEnd - 2; idx > eeprom_info.CurPageBase; idx -= 4) {
		if ((*(__IO uint16_t*)idx) == address)	{	// Find last value for address
			old_data = (*(__IO uint16_t*)(idx - 2));	// Read last data
			if (old_data == data)
				return EEPROM_OK;
			if (old_data == 0xFFFF) {
				FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, idx - 2, data);
				if (FlashStatus == HAL_OK)
					return EEPROM_OK;
			}
			break;
		}
	}

	// Check each active page address starting from begining
	for (idx = eeprom_info.CurPageBase + 4; idx < curPageEnd; idx += 4) {
		if ((*(__IO uint32_t*)idx) == 0xFFFFFFFF) {	// Verify if element contents are 0xFFFFFFFF
			FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, idx, data); 	// Set variable data
			if (FlashStatus != HAL_OK)
				return FlashStatus;

			FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, idx + 2, address); 	// Set variable data
			if (FlashStatus != HAL_OK)
				return FlashStatus;

			return EEPROM_OK;
		}
	}

	// Empty slot not found, need page transfer
	// Calculate unique variables in page
	var_num = EE_GetVariablesCount(address) + 1;
	if (var_num > ((EEPROM_PAGE_SIZE >> 2) - 1))
		return EEPROM_OUT_SIZE;

	/* transfer to swap page, and set swap page current */
	FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, eeprom_info.CurSwapPageBase, EEPROM_RECEIVE_DATA);
	if (FlashStatus != HAL_OK)
		return FlashStatus;

	// Write the variable passed as parameter in the new active page
	FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, eeprom_info.CurSwapPageBase + 4, data);
	if (FlashStatus != HAL_OK)
		return FlashStatus;

	FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, eeprom_info.CurSwapPageBase + 6, address);
	if (FlashStatus != HAL_OK)
		return FlashStatus;

	FlashStatus = EE_PageTransfer(eeprom_info.CurSwapPageBase, eeprom_info.CurPageBase, address);
	if (FlashStatus != EEPROM_OK)
		return FlashStatus;
	
	FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, eeprom_info.CurSwapPageBase, EEPROM_VALID_PAGE);
	if (FlashStatus != HAL_OK)
		return FlashStatus;

	/* sector full, erase it */
	if (eeprom_info.CurPageBase == EEPROM_SECTOR0_BASE + EEPROM_SECTOR_SIZE - EEPROM_PAGE_SIZE
		|| eeprom_info.CurPageBase == EEPROM_SECTOR1_BASE + EEPROM_SECTOR_SIZE - EEPROM_PAGE_SIZE) {

		FlashStatus = EE_EraseSector(eeprom_info.CurPageBase);
		if (FlashStatus != HAL_OK)
			return FlashStatus;

		eeprom_info.CurPageBase = eeprom_info.CurSwapPageBase;
		eeprom_info.CurSwapPageBase += EEPROM_PAGE_SIZE;
		return EEPROM_OK;
	}

	FlashStatus = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, eeprom_info.CurPageBase, EEPROM_IGNORED_PAGE);
	if (FlashStatus != HAL_OK)
		return FlashStatus;

	eeprom_info.CurPageBase = eeprom_info.CurSwapPageBase;
	if (eeprom_info.CurSwapPageBase == EEPROM_SECTOR1_BASE + EEPROM_SECTOR_SIZE - EEPROM_PAGE_SIZE) {
		eeprom_info.CurSwapPageBase = EEPROM_SECTOR0_BASE;
	} else {
		eeprom_info.CurSwapPageBase += EEPROM_PAGE_SIZE;
	}
	return EEPROM_OK;
}

/**
 * @brief  Erases PAGE0 and PAGE1 and writes EEPROM_VALID_PAGE / 0 header to PAGE0
 * @param  PAGE0 and PAGE1 base addresses
 * @retval Status of the last operation (Flash write or erase) done during EEPROM formating
 */
uint16_t EE_Format(void)
{
	uint16_t status;
	HAL_FLASH_Unlock();
	__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | 
			FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR| FLASH_FLAG_PGSERR);

	status = EE_EraseSector(EEPROM_SECTOR0_BASE);
	if (EEPROM_OK != status) {
		return status;
	}

	status = EE_EraseSector(EEPROM_SECTOR1_BASE);
	if (EEPROM_OK != status) {
		return status;
	}

	status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, EEPROM_SECTOR0_BASE, EEPROM_VALID_PAGE);
	if (HAL_OK != status) {
		return status;
	}

	eeprom_info.CurPageBase = EEPROM_SECTOR0_BASE;
	eeprom_info.CurSwapPageBase = EEPROM_SECTOR0_BASE + EEPROM_PAGE_SIZE;
	eeprom_info.Status = EEPROM_OK;
	return EEPROM_OK;
}

